/*! \file
 * \brief Enter a brief description here.
 *
 * Enter a detailed description here. (optional)
 * \author Author (Ivo Wingelaar)
 * \date Date (2011)
 * \version Version (0.0.1)
 * \copyright Copyright (GNU LGPL v3)
 */

#include "LynxShaderMaterialRenderer.hpp"
#include "LynxVideoManager_C.hpp"
#include <stdio.h>

namespace Lynx
{

//! Constructor
ShaderMaterialRenderer::ShaderMaterialRenderer(VideoManager_C* driver, int32_t& outMaterialTypeNr, const char* vertexShaderProgram, const char* pixelShaderProgram, ShaderConstantSetCallBack* callback, MaterialRenderer* baseMaterial, int32_t userData) :
    mDriver(driver),
    mCallBack(callback),
    mBaseMaterial(baseMaterial),
    mVertexShader(0),
    mUserData(userData)
{
    mPixelShader.reserve(4);
    for(uint32_t i=0; i<4; ++i)
        mPixelShader[i] = 0;

    if(mBaseMaterial)
        mBaseMaterial->increment_counter();

    if(mCallBack)
        mCallBack->increment_counter();

    init(outMaterialTypeNr, vertexShaderProgram, pixelShaderProgram, EVT_STANDARD);
}

//! constructor only for use by derived classes who want to
//! create a fall back material for example.
ShaderMaterialRenderer::ShaderMaterialRenderer(VideoManager_C* driver, ShaderConstantSetCallBack* callback, MaterialRenderer* baseMaterial, int32_t userData) :
    mDriver(driver),
    mCallBack(callback),
    mBaseMaterial(baseMaterial),
    mVertexShader(0),
    mUserData(userData)
{
    mPixelShader.reserve(4);
    for(uint32_t i=0; i<4; ++i)
        mPixelShader[i]=0;

    if(mBaseMaterial)
        mBaseMaterial->increment_counter();

    if(mCallBack)
        mCallBack->increment_counter();
}

//! Destructor
ShaderMaterialRenderer::~ShaderMaterialRenderer()
{
    if(mCallBack)
        mCallBack->decrement_counter();

    if(mVertexShader)
        mDriver->extGlDeletePrograms(1, &mVertexShader);

    for(uint32_t i=0; i<mPixelShader.size(); ++i)
        if(mPixelShader[i])
            mDriver->extGlDeletePrograms(1, &mPixelShader[i]);

    if(mBaseMaterial)
        mBaseMaterial->decrement_counter();
}

void ShaderMaterialRenderer::init(int32_t& outMaterialTypeNr, const char* vertexShaderProgram, const char* pixelShaderProgram, E_VERTEX_TYPE type)
{
    outMaterialTypeNr = -1;

    bool success;

    // create vertex shader
    success=createVertexShader(vertexShaderProgram);

    // create pixel shader
    if(!createPixelShader(pixelShaderProgram) || !success)
        return;

    // register as a new material
    outMaterialTypeNr = mDriver->add_material_renderer(this);
}

bool ShaderMaterialRenderer::on_render(MaterialRendererServices* service, E_VERTEX_TYPE vtxtype)
{
    // call callback to set shader constants
    if(mCallBack && (mVertexShader || mPixelShader[0]))
        mCallBack->on_set_constants(service, mUserData);

    return true;
}

void ShaderMaterialRenderer::on_set_material(const Material& material, const Material& lastMaterial, bool resetAllRenderstates, MaterialRendererServices* services)
{
    if(material.mMaterialType != lastMaterial.mMaterialType || resetAllRenderstates)
    {
        if(mVertexShader)
        {
            // set new vertex shader
#ifdef GL_ARB_vertex_program
            mDriver->extGlBindProgram(GL_VERTEX_PROGRAM_ARB, mVertexShader);
            glEnable(GL_VERTEX_PROGRAM_ARB);
#elif defined(GL_NV_vertex_program)
            mDriver->extGlBindProgram(GL_VERTEX_PROGRAM_NV, mVertexShader);
            glEnable(GL_VERTEX_PROGRAM_NV);
#endif
        }

        // set new pixel shader
        if(mPixelShader[0])
        {
            GLuint nextShader=mPixelShader[0];
            if(material.mFogEnable)
            {
                GLint curFogMode;
                glGetIntegerv(GL_FOG_MODE, &curFogMode);
//				if (Driver->LinearFog && PixelShader[1])
                if(curFogMode==GL_LINEAR && mPixelShader[1])
                    nextShader=mPixelShader[1];
//				else if (!Driver->LinearFog && PixelShader[2])
                else if(curFogMode==GL_EXP && mPixelShader[2])
                    nextShader=mPixelShader[2];
                else if(curFogMode==GL_EXP2 && mPixelShader[3])
                    nextShader=mPixelShader[3];
            }
#ifdef GL_ARB_fragment_program
            mDriver->extGlBindProgram(GL_FRAGMENT_PROGRAM_ARB, nextShader);
            glEnable(GL_FRAGMENT_PROGRAM_ARB);
#elif defined(GL_NV_fragment_program)
            mDriver->extGlBindProgram(GL_FRAGMENT_PROGRAM_NV, nextShader);
            glEnable(GL_FRAGMENT_PROGRAM_NV);
#endif
        }

        if(mBaseMaterial)
            mBaseMaterial->on_set_material(material, material, true, services);
    }

    //let callback know used material
    if(mCallBack)
        mCallBack->on_set_material(material);

    for(uint32_t i=0; i<8; ++i)
        mDriver->set_active_texture(i, material.get_texture(i));
    mDriver->set_basic_render_states(material, lastMaterial, resetAllRenderstates);
}

void ShaderMaterialRenderer::on_unset_material()
{
    // disable vertex shader
#ifdef GL_ARB_vertex_program
    if(mVertexShader)
        glDisable(GL_VERTEX_PROGRAM_ARB);
#elif defined(GL_NV_vertex_program)
    if(mVertexShader)
        glDisable(GL_VERTEX_PROGRAM_NV);
#endif

#ifdef GL_ARB_fragment_program
    if(mPixelShader[0])
        glDisable(GL_FRAGMENT_PROGRAM_ARB);
#elif defined(GL_NV_fragment_program)
    if(mPixelShader[0])
        glDisable(GL_FRAGMENT_PROGRAM_NV);
#endif

    if(mBaseMaterial)
        mBaseMaterial->on_unset_material();
}

//! Returns if the material is transparent.
bool ShaderMaterialRenderer::is_transparent() const
{
    return mBaseMaterial ? mBaseMaterial->is_transparent() : false;
}

// This method needs a properly cleaned error state before the checked instruction is called
bool ShaderMaterialRenderer::checkError(const char* type)
{
#if defined(GL_ARB_vertex_program) || defined(GL_NV_vertex_program) || defined(GL_ARB_fragment_program) || defined(GL_NV_fragment_program)
    GLenum g = glGetError();
    if(g == GL_NO_ERROR)
        return false;

    std::string errString = type;
    errString += " compilation failed";

    errString += " at position ";
    GLint errPos=-1;
#if defined(GL_ARB_vertex_program) || defined(GL_ARB_fragment_program)
    glGetIntegerv(GL_PROGRAM_ERROR_POSITION_ARB, &errPos);
#else
    glGetIntegerv(GL_PROGRAM_ERROR_POSITION_NV, &errPos);
#endif
    char tmpbuf[255];
    snprintf(tmpbuf, 255, "%d", int32_t(errPos));
    errString += tmpbuf;
    errString += ":\n";
#if defined(GL_ARB_vertex_program) || defined(GL_ARB_fragment_program)
    errString += reinterpret_cast<const char*>(glGetString(GL_PROGRAM_ERROR_STRING_ARB));
#else
    errString += reinterpret_cast<const char*>(glGetString(GL_PROGRAM_ERROR_STRING_NV));
#endif
#else
    std::string errString("Shaders not supported.");
#endif
    mDriver->get_log_manager()->log(errString.c_str(), ELL_ERROR);
    return true;
}

bool ShaderMaterialRenderer::createPixelShader(const char* pxsh)
{
    if(!pxsh)
        return true;

    const std::string inshdr(pxsh);
    std::string shdr;
    const int32_t pos = inshdr.find("#_IRR_FOG_MODE_");
    const uint32_t numShaders = (-1 != pos)?4:1;

    for(uint32_t i=0; i<numShaders; ++i)
    {
        if(i==0)
        {
            shdr=inshdr;
        }
        else
        {
            shdr = inshdr.substr(0, pos);
            switch(i)
            {
            case 1:
                shdr += "OPTION ARB_fog_linear;";
                break;
            case 2:
                shdr += "OPTION ARB_fog_exp;";
                break;
            case 3:
                shdr += "OPTION ARB_fog_exp2;";
                break;
            }
            shdr += inshdr.substr(pos+16, inshdr.size()-pos-16);
        }
        mDriver->extGlGenPrograms(1, &mPixelShader[i]);
#ifdef GL_ARB_fragment_program
        mDriver->extGlBindProgram(GL_FRAGMENT_PROGRAM_ARB, mPixelShader[i]);
#elif defined GL_NV_fragment_program
        mDriver->extGlBindProgram(GL_FRAGMENT_PROGRAM_NV, mPixelShader[i]);
#endif

        // clear error buffer
        while(glGetError() != GL_NO_ERROR)
            {}

#ifdef GL_ARB_fragment_program
        // compile
        mDriver->extGlProgramString(GL_FRAGMENT_PROGRAM_ARB, GL_PROGRAM_FORMAT_ASCII_ARB,
                                    shdr.size(), shdr.c_str());
#elif defined GL_NV_fragment_program
        mDriver->extGlLoadProgram(GL_FRAGMENT_PROGRAM_NV, mPixelShader[i],
                                  shdr.size(), shdr.c_str());
#endif

        if(checkError("Pixel shader"))
        {
            mDriver->extGlDeletePrograms(1, &mPixelShader[i]);
            mPixelShader[i]=0;

            return false;
        }
    }

    return true;
}

bool ShaderMaterialRenderer::createVertexShader(const char* vtxsh)
{
    if(!vtxsh)
        return true;

    mDriver->extGlGenPrograms(1, &mVertexShader);
#ifdef GL_ARB_vertex_program
    mDriver->extGlBindProgram(GL_VERTEX_PROGRAM_ARB, mVertexShader);
#elif defined GL_NV_vertex_program
    mDriver->extGlBindProgram(GL_VERTEX_PROGRAM_NV, mVertexShader);
#endif

    // clear error buffer
    while(glGetError() != GL_NO_ERROR)
        {}

    // compile
#ifdef GL_ARB_vertex_program
    mDriver->extGlProgramString(GL_VERTEX_PROGRAM_ARB, GL_PROGRAM_FORMAT_ASCII_ARB,
                                (GLsizei)strlen(vtxsh), vtxsh);
#elif defined GL_NV_vertex_program
    mDriver->extGlLoadProgram(GL_VERTEX_PROGRAM_NV, mVertexShader,
                              (GLsizei)strlen(vtxsh), vtxsh);
#endif

    if(checkError("Vertex shader"))
    {
        mDriver->extGlDeletePrograms(1, &mVertexShader);
        mVertexShader=0;

        return false;
    }

    return true;
}

} // namespace Lynx
